/*
 * Copyright 2019 Flipkart Internet Pvt. Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.flipkart.ohos.proteus;

import ohos.app.Context;

import com.flipkart.ohos.proteus.parser.ParseHelper;
import com.flipkart.ohos.proteus.toolbox.Utils;
import com.flipkart.ohos.proteus.value.Array;
import com.flipkart.ohos.proteus.value.Primitive;
import com.flipkart.ohos.proteus.value.Value;

import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author Aditya Sharat on 18-05-2015.
 */
public abstract class Function {
    // SPECIAL
    public static final Function NOOP = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            return ProteusConstants.EMPTY_STRING;
        }

        @Override
        public String getName() {
            return "noop";
        }
    };

    public static final Function DATE = new Function() {
        private final SimpleDateFormat from = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        private final SimpleDateFormat to = new SimpleDateFormat("E, d MMM");

        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            Date in = getFromFormat(arguments).parse(arguments[0].getAsString());
            String out = getToFormat(arguments).format(in);
            return new Primitive(out);
        }

        private SimpleDateFormat getFromFormat(Value[] arguments) {
            if (arguments.length > 2) {
                return new SimpleDateFormat(arguments[2].getAsString());
            } else {
                return from;
            }
        }

        private SimpleDateFormat getToFormat(Value[] arguments) {
            if (arguments.length > 1) {
                return new SimpleDateFormat(arguments[1].getAsString());
            } else {
                return to;
            }
        }

        @Override
        public String getName() {
            return "date";
        }
    };

    public static final Function FORMAT = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            String template = arguments[0].getAsString();
            String[] values = new String[arguments.length - 1];
            for (int i = 1; i < arguments.length; i++) {
                values[i - 1] = arguments[i].getAsString();
            }
            return new Primitive(String.format(template, (Object[]) values));
        }

        @Override
        public String getName() {
            return "format";
        }
    };

    public static final Function JOIN = new Function() {
        private static final String DEFAULT_DELIMITER = ", ";

        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            return new Primitive(Utils.join(arguments[0].getAsArray(), getDelimiter(arguments)));
        }

        private String getDelimiter(Value[] arguments) {
            if (arguments.length > 1) {
                return arguments[1].getAsString();
            }
            return DEFAULT_DELIMITER;
        }

        @Override
        public String getName() {
            return "join";
        }
    };

    public static final Function NUMBER = new Function() {
        private final DecimalFormat DEFAULT_FORMATTER = new DecimalFormat("#,###");

        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            DecimalFormat formatter = getFormatter(arguments);
            formatter.setRoundingMode(RoundingMode.FLOOR);
            formatter.setMinimumFractionDigits(0);
            formatter.setMaximumFractionDigits(2);
            double number = Double.parseDouble(arguments[0].getAsString());
            return new Primitive(formatter.format(number));
        }

        private DecimalFormat getFormatter(Value[] arguments) {
            if (arguments.length > 1) {
                return new DecimalFormat(arguments[1].getAsString());
            }
            return DEFAULT_FORMATTER;
        }

        @Override
        public String getName() {
            return "number";
        }
    };

    public static final Function ADD = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double sum = 0;

            for (Value argument : arguments) {
                sum = sum + argument.getAsDouble();
            }
            return new Primitive(sum);
        }

        @Override
        public String getName() {
            return "add";
        }
    };

    public static final Function SUBTRACT = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double sum = arguments[0].getAsDouble();

            for (int i = 1; i < arguments.length; i++) {
                sum = sum - arguments[i].getAsDouble();
            }
            return new Primitive(sum);
        }

        @Override
        public String getName() {
            return "sub";
        }
    };

    public static final Function MULTIPLY = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double product = 1;

            for (Value argument : arguments) {
                product = product * argument.getAsDouble();
            }
            return new Primitive(product);
        }

        @Override
        public String getName() {
            return "mul";
        }
    };

    public static final Function DIVIDE = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double quotient = arguments[0].getAsDouble();

            for (int i = 1; i < arguments.length; i++) {
                quotient = quotient / arguments[i].getAsDouble();
            }

            return new Primitive(quotient);
        }

        @Override
        public String getName() {
            return "div";
        }
    };

    public static final Function MODULO = new Function() {
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double remainder = arguments[0].getAsDouble();

            for (int i = 1; i < arguments.length; i++) {
                remainder = remainder % arguments[i].getAsDouble();
            }

            return new Primitive(remainder);
        }

        @Override
        public String getName() {
            return "mod";
        }
    };

    public static final Function AND = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 1) {
                return ProteusConstants.FALSE;
            }
            boolean bool = true;
            for (Value argument : arguments) {
                bool = ParseHelper.parseBoolean(argument);
                if (!bool) {
                    break;
                }
            }
            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "and";
        }
    };

    public static final Function OR = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 1) {
                return ProteusConstants.FALSE;
            }
            boolean bool = false;
            for (Value argument : arguments) {
                bool = ParseHelper.parseBoolean(argument);
                if (bool) {
                    break;
                }
            }
            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "or";
        }
    };

    public static final Function NOT = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 1) {
                return ProteusConstants.TRUE;
            }
            return ParseHelper.parseBoolean(arguments[0]) ? ProteusConstants.FALSE : ProteusConstants.TRUE;
        }

        @Override
        public String getName() {
            return "not";
        }
    };

    public static final Function EQUALS = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 2) {
                return ProteusConstants.FALSE;
            }

            Value x = arguments[0];
            Value y = arguments[1];
            boolean bool = false;
            if (x.isPrimitive() && y.isPrimitive()) {
                bool = x.getAsPrimitive().equals(y.getAsPrimitive());
            }
            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "eq";
        }
    };

    public static final Function LESS_THAN = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 2) {
                return ProteusConstants.FALSE;
            }

            Value x = arguments[0];
            Value y = arguments[1];
            boolean bool = false;
            if (x.isPrimitive() && y.isPrimitive()) {
                bool = x.getAsPrimitive().getAsDouble() < y.getAsPrimitive().getAsDouble();
            }

            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "lt";
        }
    };

    public static final Function GREATER_THAN = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 2) {
                return ProteusConstants.FALSE;
            }

            Value x = arguments[0];
            Value y = arguments[1];
            boolean bool = false;
            if (x.isPrimitive() && y.isPrimitive()) {
                bool = x.getAsPrimitive().getAsDouble() > y.getAsPrimitive().getAsDouble();
            }
            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "gt";
        }
    };

    public static final Function LESS_THAN_OR_EQUALS = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 2) {
                return ProteusConstants.FALSE;
            }

            Value x = arguments[0];
            Value y = arguments[1];
            boolean bool = false;
            if (x.isPrimitive() && y.isPrimitive()) {
                bool = x.getAsPrimitive().getAsDouble() <= y.getAsPrimitive().getAsDouble();
            }

            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "lte";
        }
    };

    public static final Function GREATER_THAN_OR_EQUALS = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            if (arguments.length < 2) {
                return ProteusConstants.FALSE;
            }

            Value x = arguments[0];
            Value y = arguments[1];
            boolean bool = false;
            if (x.isPrimitive() && y.isPrimitive()) {
                bool = x.getAsPrimitive().getAsDouble() >= y.getAsPrimitive().getAsDouble();
            }
            return bool ? ProteusConstants.TRUE : ProteusConstants.FALSE;
        }

        @Override
        public String getName() {
            return "gte";
        }
    };

    // Conditional

    public static final Function TERNARY = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            Value i = arguments[0];
            Value t = arguments[1];
            Value e = arguments[2];
            return ParseHelper.parseBoolean(i) ? t : e;
        }

        @Override
        public String getName() {
            return "ternary";
        }
    };

    // String

    //String.charAt()
    public static final Function CHAR_AT = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            String string = arguments[0].getAsString();
            int index = arguments[1].getAsInt();
            char charAtIndex = string.charAt(index);
            return new Primitive(charAtIndex);
        }

        @Override
        public String getName() {
            return "charAt";
        }
    };

    //String.contains()
    public static final Function CONTAINS = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            String string = arguments[0].getAsString();
            String substring = arguments[1].getAsString();
            boolean bool = string.contains(substring);
            return new Primitive(bool);
        }

        @Override
        public String getName() {
            return "contains";
        }
    };

    //String.endsWith()
    //String.indexOf()

    //String.isEmpty()
    public static final Function IS_EMPTY = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            String string = arguments[0].getAsString();
            return new Primitive(ProteusConstants.EMPTY.equals(string));
        }

        @Override
        public String getName() {
            return "isEmpty";
        }
    };

    //String.lastIndexOf()

    //String.length()
    public static final Function LENGTH = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            Value value = arguments[0];
            int length = 0;
            if (value.isPrimitive()) {
                length = value.getAsString().length();
            } else if (value.isArray()) {
                length = value.getAsArray().size();
            }
            return new Primitive(length);
        }

        @Override
        public String getName() {
            return "length";
        }
    };

    //String.matches()
    //String.replace()
    //String.replaceAll()
    //String.replaceFirst()
    //String.split()
    //String.startsWith()
    //String.substring()
    //String.toLowerCase()
    //String.toUpperCase()

    //String.trim()
    public static final Function TRIM = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            String string = arguments[0].getAsString().trim();
            return new Primitive(string);
        }

        @Override
        public String getName() {
            return "trim";
        }
    };

    //String.subSequence()

    // Math

    //Math.abs
    //Math.ceil
    //Math.floor
    //Math.pow
    //Math.round
    //Math.random

    //Math.max
    public static final Function MAX = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double max = arguments[0].getAsDouble();
            double current;
            for (int i = 1; i < arguments.length; i++) {
                current = arguments[i].getAsDouble();
                if (current > max) {
                    max = current;
                }
            }
            return new Primitive(max);
        }

        @Override
        public String getName() {
            return "max";
        }
    };

    //Math.min
    public static final Function MIN = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            double min = arguments[0].getAsDouble();
            double current;
            for (int i = 1; i < arguments.length; i++) {
                current = arguments[i].getAsDouble();
                if (current < min) {
                    min = current;
                }
            }
            return new Primitive(min);
        }

        @Override
        public String getName() {
            return "min";
        }
    };

    // Array

    public static final Function SLICE = new Function() {
        @Override
        public Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception {
            Array in = arguments[0].getAsArray();
            int start = getStart(in, arguments);
            int end = getEnd(in, arguments);
            Array out = new Array();
            for (int i = start; i < end; i++) {
                out.add(in.get(i));
            }
            return out;
        }

        private int getStart(Array in, Value[] arguments) {
            if (arguments.length > 1) {
                int index = arguments[1].getAsInt();
                if (index < 0) {
                    index = in.size() - index;
                    if (index < 0) {
                        return 0;
                    }
                } else if (index > in.size()) {
                    index = in.size();
                }
                return index;
            }
            return 0;
        }

        private int getEnd(Array in, Value[] arguments) {
            if (arguments.length > 2) {
                int index = arguments[2].getAsInt();
                if (index < 0) {
                    index = in.size() - index;
                    if (index < 0) {
                        return 0;
                    }
                } else if (index > in.size()) {
                    index = in.size();
                }
                return index;
            }
            return in.size();
        }

        @Override
        public String getName() {
            return "slice";
        }
    };


    public abstract Value call(Context context, Value data, int dataIndex, Value... arguments) throws Exception;

    public abstract String getName();
}
